# Table of Contents

# Query DSL

Hibernate는 복잡한 쿼리 또는 조인을 처리하기 위해 JPQL을 제공한다. 그러나 JPQL은 큰 단점이 하나 있다. 예제를 살펴보자.

 


String jpql = "select m from MemberEntity as m";
List<MemberEntity> members = entityManager.createQuery(jpql, MemberEntity.class).getResultList();

JPQL은 문자열로 작성한다. 따라서 문법적 오류가 있어도 코드 작성 시점이나 컴파일 타임에 알아낼 수 없다.

Spring Data JPA의 JPQL도 같은 단점이 있다.






 


import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
    @Query("select m from MemberEntity where m.email = :email and m.name = :name")
    List<MemberEntity> findByEmailAndName(@Param("email") String email, @Param("name") String name);

이러한 문제를 해결하기 위해 Query DSL(Domain Specific Language)가 등장했다. Query DSL은 다음과 같은 장점이 있다.

  • 문자열이 아닌 코드로 작성한다.
  • 문법적 오류를 컴파일 단계에서 탐지할 수 있으므로 타입 안정성이 있다.
  • 쿼리 결과를 엔티티가 아닌 사용자 정의 객체로 받을 수 있다.

# 의존성 추가

Spring Boot, Gradle 버전에 따라 Query DSL 설정 방법이 조금씩 다르다. 이 포스트에서는 다음 버전을 기준으로 한다.

  • Gradle 7.1.1
  • Spring Boot 2.5.3

build.gradle를 작성하자. 어두운 부분이 Query DSL과 관련된 설정이다.





 








 






 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

plugins {
    id 'org.springframework.boot' version '2.5.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
}

// 생략 ...

dependencies {
    // 생략 ...

    // QueryDSL
    implementation "com.querydsl:querydsl-jpa"
}

tasks.named('test') {
    useJUnitPlatform()
}

def querydslDir = "$buildDir/generated/querydsl"

querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}

sourceSets {
    main.java.srcDir querydslDir
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    querydsl.extendsFrom compileClasspath
}

compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}

# Q 클래스 생성하기

Query DSL은 컴파일 시점에 엔티티에 접두사Q를 붙인 클래스를 생성한다. 만약 엔티티의 클래스 이름이 MemberEntity라면 QMemberEntity라는 Q 클래스가 생성된다.

엔티티 클래스 MemberEntity를 다음과 같이 정의하자.

@Entity
@Table(name= "member")
@Getter
@NoArgsConstructor
public class MemberEntity {

    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String name;

    @Column
    private Integer age;

    @Column
    private String nation;

    @Column
    private String gender;

    @Column(nullable = false)
    private String password;

    @OneToMany(mappedBy = "writer")
    private List<PostEntity> posts;

    @Builder
    public MemberEntity(String email, String name, Integer age, String nation, String gender, String password) {
        this.email = email;
        this.name = name;
        this.age = age;
        this.nation = nation;
        this.gender = gender;
        this.password = password;
    }
}

PostEntity 클래스도 정의하자.

@Entity
@Table(name = "post")
@Getter
@NoArgsConstructor
public class PostEntity {

    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column
    private String content;

    @ManyToOne
    @JoinColumn(name = "writer_id")
    private MemberEntity writer;

    @Builder
    public PostEntity(Long id, String content) {
        this.id = id;
        this.content = content;
    }
}

이제 Q 클래스를 생성하자. Gradle > Tasks > build > cleanGradle > Tasks > other > compileQuerydsl을 순서대로 클릭하여 실행한다.

물론 터미널에서도 실행할 수 있다.

./gradlew clean
./gradlew compileQuerydsl

그러면 다음 경로에 QMemberEntity와 QPostEntity클래스가 생성된다.

// QMemberEntity.java
@Generated("com.querydsl.codegen.EntitySerializer")
public class QMemberEntity extends EntityPathBase<MemberEntity> {

    private static final long serialVersionUID = 955017021L;

    public static final QMemberEntity memberEntity = new QMemberEntity("memberEntity");

    public final NumberPath<Integer> age = createNumber("age", Integer.class);

    public final StringPath email = createString("email");

    public final StringPath gender = createString("gender");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath name = createString("name");

    public final StringPath nation = createString("nation");

    public final StringPath password = createString("password");

    public final ListPath<com.yologger.project.repository.post.PostEntity, com.yologger.project.repository.post.QPostEntity> posts = this.<com.yologger.project.repository.post.PostEntity, com.yologger.project.repository.post.QPostEntity>createList("posts", com.yologger.project.repository.post.PostEntity.class, com.yologger.project.repository.post.QPostEntity.class, PathInits.DIRECT2);

    public QMemberEntity(String variable) {
        super(MemberEntity.class, forVariable(variable));
    }

    public QMemberEntity(Path<? extends MemberEntity> path) {
        super(path.getType(), path.getMetadata());
    }

    public QMemberEntity(PathMetadata metadata) {
        super(MemberEntity.class, metadata);
    }

}
// QPostEntity.java
@Generated("com.querydsl.codegen.EntitySerializer")
public class QPostEntity extends EntityPathBase<PostEntity> {

    private static final long serialVersionUID = 1109069309L;

    private static final PathInits INITS = PathInits.DIRECT2;

    public static final QPostEntity postEntity = new QPostEntity("postEntity");

    public final StringPath content = createString("content");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final com.yologger.project.repository.member.QMemberEntity writer;

    public QPostEntity(String variable) {
        this(PostEntity.class, forVariable(variable), INITS);
    }

    public QPostEntity(Path<? extends PostEntity> path) {
        this(path.getType(), path.getMetadata(), PathInits.getFor(path.getMetadata(), INITS));
    }

    public QPostEntity(PathMetadata metadata) {
        this(metadata, PathInits.getFor(metadata, INITS));
    }

    public QPostEntity(PathMetadata metadata, PathInits inits) {
        this(PostEntity.class, metadata, inits);
    }

    public QPostEntity(Class<? extends PostEntity> type, PathMetadata metadata, PathInits inits) {
        super(type, metadata, inits);
        this.writer = inits.isInitialized("writer") ? new com.yologger.project.repository.member.QMemberEntity(forProperty("writer")) : null;
    }

}

개발 과정에서 Q 클래스가 필요하므로 위와 같은 방법으로 생성해준다. 다만 빌드 과정에 Q 클래스 생성 작업이 포함되기에 굳이 git에 포함시킬 필요는 없다.

# Query DSL 구성 클래스 작성

Query DSL을 사용하려면 JPAQueryFactory객체를 빈으로 등록해야한다.














 
 
 
 


import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Configuration
public class QueryDslConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

Query DSL은 내부적으로 EntityManager를 사용한다. 이 Entity Manager를 JPAQueryFactory클래스 생성자로 전달한 후 빈으로 등록하면 된다.

# Query DSL과 영속성 컨텍스트

Query DSL은 내부적으로 JPA의 EntityManager를 사용한다. 따라서 Query DSL로 조회한 엔티티도 JPA의 영속성 컨텍스트에서 관리된다.

# 데이터 조회

빈으로 등록한 JpaQueryFactory 객체로 복잡한 데이터 조회 및 조인 작업이 가능하다.

# selectFrom(), fetch()

selectFrom()으로 엔티티를 지정하고 fetch()를 호출하면 모든 데이터가 조회된다.



 


































 





// 생략 ...

import static com.yologger.project.repository.member.QMemberEntity.memberEntity;

@SpringBootTest
class MemberEntityTest {

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    JPAQueryFactory jpaQueryFactory;

    @AfterEach
    public void tearDown() {
        memberRepository.deleteAll();
    }

    @Test
    public void test() {

        List<MemberEntity> dummyMembers = Arrays.asList(
                MemberEntity.builder().email("paul@gmail.com").name("paul").age(34).nation("usa").password("1234").build(),
                MemberEntity.builder().email("john@gmail.com").name("john").age(23).nation("usa").password("1234").build(),
                MemberEntity.builder().email("smith@gmail.com").name("smith").age(60).nation("usa").password("1234").build(),
                MemberEntity.builder().email("jane@gmail.com").name("jane").age(45).nation("usa").password("1234").build(),
                MemberEntity.builder().email("ross@gmail.com").name("ross").age(15).nation("korea").password("1234").build(),
                MemberEntity.builder().email("kane@gmail.com").name("kane").age(51).nation("korea").password("1234").build(),
                MemberEntity.builder().email("monica@gmail.com").name("monica").age(39).nation("korea").password("1234").build(),
                MemberEntity.builder().email("ramos@gmail.com").name("ramos").age(51).nation("korea").password("1234").build(),
                MemberEntity.builder().email("chandler@gmail.com").name("chandler").age(60).nation("france").password("1234").build(),
                MemberEntity.builder().email("rachel@gmail.com").name("rachel").age(72).nation("france").password("1234").build(),
                MemberEntity.builder().email("messi@gmail.com").name("messi").age(22).nation("france").password("1234").build()
        );

        memberRepository.saveAll(dummyMembers);

        List<MemberEntity> list = jpaQueryFactory.selectFrom(memberEntity).fetch();

        assertThat(list.size()).isEqualTo(11);
    }
}

로그에 출력되는 실제 SQL 쿼리는 다음과 같다.

Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_

fetch() 외에도 데이터를 조회하는 다양한 메소드가 있다.

  • fetch(): 모든 데이터를 조회
  • fecthOne(): 데이터 한개 조회
  • fetchFirst(): 첫 번째 데이터 조회
  • fetchCount(): 데이터 개수 반환

# where()

where()로 조건을 추가할 수 있다.

# eq()

List<MemberEntity> dummyMembers = Arrays.asList(
    MemberEntity.builder().email("paul@gmail.com").name("paul").age(34).nation("usa").password("1234").build(),
    MemberEntity.builder().email("john@gmail.com").name("john").age(23).nation("usa").password("1234").build(),
    MemberEntity.builder().email("smith@gmail.com").name("smith").age(60).nation("usa").password("1234").build(),
    MemberEntity.builder().email("jane@gmail.com").name("jane").age(45).nation("usa").password("1234").build(),
    MemberEntity.builder().email("ross@gmail.com").name("ross").age(15).nation("korea").password("1234").build(),
    MemberEntity.builder().email("kane@gmail.com").name("kane").age(51).nation("korea").password("1234").build(),
    MemberEntity.builder().email("monica@gmail.com").name("monica").age(39).nation("korea").password("1234").build(),
    MemberEntity.builder().email("ramos@gmail.com").name("ramos").age(51).nation("korea").password("1234").build(),
    MemberEntity.builder().email("chandler@gmail.com").name("chandler").age(60).nation("france").password("1234").build(),
    MemberEntity.builder().email("rachel@gmail.com").name("rachel").age(72).nation("france").password("1234").build(),
    MemberEntity.builder().email("messi@gmail.com").name("messi").age(22).nation("france").password("1234").build()
);

memberRepository.saveAll(dummyMembers);

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.eq("korea"))
    .fetch();

실행되는 SQL 쿼리는 다음과 같다.

Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_ 
    where
        memberenti0_.nation=?

# gt()

List<MemberEntity> dummyMembers = Arrays.asList(
    MemberEntity.builder().email("paul@gmail.com").name("paul").age(34).nation("usa").password("1234").build(),
    MemberEntity.builder().email("john@gmail.com").name("john").age(23).nation("usa").password("1234").build(),
    MemberEntity.builder().email("smith@gmail.com").name("smith").age(60).nation("usa").password("1234").build(),
    MemberEntity.builder().email("jane@gmail.com").name("jane").age(45).nation("usa").password("1234").build(),
    MemberEntity.builder().email("ross@gmail.com").name("ross").age(15).nation("korea").password("1234").build(),
    MemberEntity.builder().email("kane@gmail.com").name("kane").age(51).nation("korea").password("1234").build(),
    MemberEntity.builder().email("monica@gmail.com").name("monica").age(39).nation("korea").password("1234").build(),
    MemberEntity.builder().email("ramos@gmail.com").name("ramos").age(51).nation("korea").password("1234").build(),
    MemberEntity.builder().email("chandler@gmail.com").name("chandler").age(60).nation("france").password("1234").build(),
    MemberEntity.builder().email("rachel@gmail.com").name("rachel").age(72).nation("france").password("1234").build(),
    MemberEntity.builder().email("messi@gmail.com").name("messi").age(22).nation("france").password("1234").build()
);

memoRepository.saveAll(dummyMembers);

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.age.gt(40))
    .fetch();

# gte()

List<MemberEntity> result = jpaQueryFactory
        .selectFrom(memberEntity)
        .where(memberEntity.age.gt(30))
        .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_ 
    where
        memberenti0_.age>?

# lt()

List<MemberEntity> result = jpaQueryFactory
        .selectFrom(memberEntity)
        .where(memberEntity.age.lt(30))
        .fetch();

# lte()

List<MemberEntity> result = jpaQueryFactory
        .selectFrom(memberEntity)
        .where(memberEntity.age.lte(30))
        .fetch();

# between()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.age.between(20, 40))
    .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_ 
    where
        memberenti0_.age between ? and ?

# startWith()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.name.startsWith("j"))
    .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_ 
    where
        memberenti0_.name like ? escape '!'

# endWith()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.name.endWith("an"))
    .fetch();

# contains()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.name.contains("an"))
    .fetch();

# in()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.in("usa, korea"))
    .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.gender as gender4_0_,
        memberenti0_.name as name5_0_,
        memberenti0_.nation as nation6_0_,
        memberenti0_.password as password7_0_ 
    from
        member memberenti0_ 
    where
        memberenti0_.nation in (
            ? , ?
        )

# notIn()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.notIn("usa, korea"))
    .fetch();
List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.notIn("usa, korea"))
    .fetch();

# and()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.eq("usa").and(memberEntity.nation.eq("korea")))
    .fetch();

# or()

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.eq("usa").or(memberEntity.nation.eq("korea")))
    .fetch();

# orderBy()

orderBy()로 결과값을 정렬할 수 있다.

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .orderBy(memberEntity.nation.asc(), memberEntity.age.desc())
    .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.name as name4_0_,
        memberenti0_.nation as nation5_0_,
        memberenti0_.password as password6_0_ 
    from
        member memberenti0_ 
    order by
        memberenti0_.nation asc,
        memberenti0_.age desc

# Paging

offset()limit()를 사용하면 페이징 처리를 할 수 있다.

List<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .offset(1) 
    .limit(3)
    .fetch();
Hibernate: 
    select
        memberenti0_.id as id1_0_,
        memberenti0_.age as age2_0_,
        memberenti0_.email as email3_0_,
        memberenti0_.name as name4_0_,
        memberenti0_.nation as nation5_0_,
        memberenti0_.password as password6_0_ 
    from
        member memberenti0_ limit ? offset ?

실제 페이징 처리를 하려면 전체 데이터 개수를 알아야한다. 이때는 fetchResults() 메소드와 QueryResults클래스를 사용한다.

QueryResults<MemberEntity> result = jpaQueryFactory
    .selectFrom(memberEntity)
    .offset(1)
    .limit(3)
    .fetchResults();

Long total = result.getTotal();     // 전체 데이터 수
Long limit = result.getLimit();
Long offset = result.getOffset();
List<MemberEntity> members = result.getResults();   // 실제 데이터

assertThat(members.size()).isEqualTo(3);

# Projection

SELECT 절에 조회 컬럼을 지정하는 것을 Projection이라고 한다. 이 때는 select()from()을 사용한다.

조회 컬럼이 하나라면 해당 타입을 반환한다.

List<MemberEntity> dummyMembers = Arrays.asList(
    MemberEntity.builder().email("paul@gmail.com").name("paul").age(34).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("john@gmail.com").name("john").age(23).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("smith@gmail.com").name("smith").age(60).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("jane@gmail.com").name("jane").age(45).nation("usa").gender("woman").password("1234").build(),
    MemberEntity.builder().email("ross@gmail.com").name("ross").age(15).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("kane@gmail.com").name("kane").age(51).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("monica@gmail.com").name("monica").age(39).nation("korea").gender("woman").password("1234").build(),
    MemberEntity.builder().email("ramos@gmail.com").name("ramos").age(51).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("chandler@gmail.com").name("chandler").age(60).nation("france").gender("man").password("1234").build(),
    MemberEntity.builder().email("rachel@gmail.com").name("rachel").age(72).nation("france").gender("woman").password("1234").build(),
    MemberEntity.builder().email("messi@gmail.com").name("messi").age(22).nation("france").gender("man").password("1234").build()
);
memberRepository.saveAll(dummyMembers);

List<String> emails = jpaQueryFactory
    .select(memberEntity.email)
    .from(memberEntity)
    .fetch();

조회 컬럼이 여러 개일 때는 Tuple을 사용한다.

List<Tuple> tuples = jpaQueryFactory
    .select(memberEntity.email, memberEntity.age)
    .from(memberEntity)
    .fetch();

for (Tuple tuple: tuples) {
    String email = tuple.get(memberEntity.email);
    Integer age = tuple.get(memberEntity.age);
} 

쿼리 결과를 특정 객체로 매핑할 수도 있다.

@NoArgsConstructor
@Getter
public class MemberDTO {
    private String email;
    private Integer age;
    private String nation;
    private String gender;

    @Builder
    public MemberDTO(String email, Integer age, String nation, String gender) {
        this.email = email;
        this.age = age;
        this.nation = nation;
        this.gender = gender;
    }
}
List<MemberDTO> members = jpaQueryFactory
    .select(Projections.constructor(MemberDTO.class, memberEntity.email, memberEntity.age, memberEntity.nation, memberEntity.gender))
    .from(memberEntity)
    .fetch();

# Grouping

결과를 그룹화하고 결과를 제한할 때는 groupBy()having()을 사용한다.

@NoArgsConstructor
@Getter
public class NationCountDTO {
    private String nation;
    private Integer count;
}
List<MemberEntity> dummyMembers = Arrays.asList(
    MemberEntity.builder().email("paul@gmail.com").name("paul").age(34).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("john@gmail.com").name("john").age(23).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("smith@gmail.com").name("smith").age(60).nation("usa").gender("man").password("1234").build(),
    MemberEntity.builder().email("jane@gmail.com").name("jane").age(45).nation("usa").gender("woman").password("1234").build(),
    MemberEntity.builder().email("ross@gmail.com").name("ross").age(15).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("kane@gmail.com").name("kane").age(51).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("monica@gmail.com").name("monica").age(39).nation("korea").gender("woman").password("1234").build(),
    MemberEntity.builder().email("ramos@gmail.com").name("ramos").age(51).nation("korea").gender("man").password("1234").build(),
    MemberEntity.builder().email("chandler@gmail.com").name("chandler").age(60).nation("france").gender("man").password("1234").build(),
    MemberEntity.builder().email("rachel@gmail.com").name("rachel").age(72).nation("france").gender("woman").password("1234").build(),
    MemberEntity.builder().email("messi@gmail.com").name("messi").age(22).nation("france").gender("man").password("1234").build()
);
memberRepository.saveAll(dummyMembers);

List<NationCountDTO> members = jpaQueryFactory
    .select(Projections.constructor(NationCountDTO.class, memberEntity.nation, memberEntity.id.count()))
    .from(memberEntity)
    .groupBy(memberEntity.nation)
    .having(memberEntity.id.count().gt(3))
    .fetch();

# Subquery

Subquery를 구현할 때는 JPAExpressions을 사용한다.

List<MemberEntity> members = jpaQueryFactory
    .selectFrom(memberEntity)
    .where(memberEntity.nation.eq(
        JPAExpressions
                .select(memberEntity.nation)
                .from(memberEntity)
                .where(memberEntity.name.eq("kane"))
    ))
    .fetch();

# Join

# Inner join

List<Tuple> tuples = jpaQueryFactory
        .select(memberEntity.email, postEntity.content)
        .from(memberEntity)
        .join(postEntity)
        .on(memberEntity.id.eq(postEntity.writer.id))
        .fetch();

assertThat(0).isEqualTo(8);

# Left join

List<Tuple> tuples = jpaQueryFactory
        .select(memberEntity.email, postEntity.content)
        .from(memberEntity)
        .leftJoin(postEntity)
        .on(memberEntity.id.eq(postEntity.writer.id))
        .fetch();

# Right join

List<Tuple> tuples = jpaQueryFactory
        .select(memberEntity.email, postEntity.content)
        .from(memberEntity)
        .rightJoin(postEntity)
        .on(memberEntity.id.eq(postEntity.writer.id))
        .fetch();

# Repository와 함께 사용하기

Query DSL은 Spring Data JPA의 Repository 인터페이스와 함께 사용할 수 있다.

public interface PostCustomRepository {
    List<PostEntity> findAllPostsOrderByCreatedAtDescExceptBlocking(Long memberId, int offset, int limit);
    List<PostEntity> findAllByWriterId(Long memberId, int offset, int limit);
}



 



























@RequiredArgsConstructor
public class PostCustomRepositoryImpl implements PostCustomRepository {

    private final JPAQueryFactory jpaQueryFactory;

    @Override
    public List<PostEntity> findAllPostsOrderByCreatedAtDescExceptBlocking(Long memberId, int page, int size) {
        return jpaQueryFactory.selectFrom(postEntity)
                .where(postEntity.writer.id.notIn(
                        JPAExpressions
                                .select(blockEntity.blocking.id)
                                .from(blockEntity)
                                .where(blockEntity.member.id.eq(memberId))
                ))
                .offset(page*size)
                .limit(size)
                .orderBy(postEntity.createdAt.desc())
                .fetch();
    }

    @Override
    public List<PostEntity> findAllByWriterId(Long memberId, int page, int size) {
        return jpaQueryFactory.selectFrom(postEntity)
                .where(postEntity.writer.id.eq(memberId))
                .offset(page*size)
                .limit(size)
                .orderBy(postEntity.createdAt.desc())
                .fetch();
    }
}
public interface PostRepository extends JpaRepository<PostEntity, Long>, PostCustomRepository {
    
}